/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.jpm.jroguefx.levels;

import com.jpm.jroguefx.Player;
import com.jpm.jroguefx.types.*;
import com.jpm.jroguefx.utilities.PerfLogger;
import com.jpm.jroguefx.utilities.Utilities;

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Outside extends Area {

    private final int XLIMIT = 1000, YLIMIT = 1000;
    private final ArrayList<Coords> houses = new ArrayList<>();
    private final Logger logger = Logger.getLogger(this.getClass().getName());


    @Override
    public int getXlimit() {
        return XLIMIT;
    }

    @Override
    public int getYlimit() {
        return YLIMIT;
    }

    public Outside() {

        tileMap = new Tile[XLIMIT][YLIMIT];

        for (int x = 0; x < XLIMIT; x++) {
            for (int y = 0; y < YLIMIT; y++) {
                tileMap[x][y] = new Tile();
            }
        }

        fillArea(0, 10, 0, YLIMIT, TerrainType.SEA2);
        fillArea(10, 323, 0, 10, TerrainType.SEA2);
        fillArea(10, 323, YLIMIT - 10, YLIMIT, TerrainType.SEA2);
        fillArea(XLIMIT - 10, XLIMIT, 0, YLIMIT, TerrainType.SEA);
        fillArea(323, XLIMIT - 10, 0, 10, TerrainType.SEA);
        fillArea(323, XLIMIT - 10, YLIMIT - 10, YLIMIT, TerrainType.SEA);
        fillArea(30, 310, 30, YLIMIT - 30, TerrainType.DESERT);
        fillArea(323, 660, 30, YLIMIT - 30, TerrainType.GRASS);
        fillArea(690, XLIMIT - 29, 30, YLIMIT - 30, TerrainType.ICE);

        fillGradientAreaX(10, 30, 30, YLIMIT - 30, TerrainType.SEA2, TerrainType.DESERT);
        fillGradientAreaX(660, 690, 30, YLIMIT - 30, TerrainType.GRASS, TerrainType.ICE);
        fillGradientAreaX(310, 340, 30, YLIMIT - 30, TerrainType.DESERT, TerrainType.GRASS);
        fillGradientAreaX(XLIMIT - 10, XLIMIT - 30, 30, YLIMIT - 30, TerrainType.SEA, TerrainType.ICE);
        fillGradientAreaY(10, 325, 10, 30, TerrainType.SEA2, TerrainType.DESERT);
        fillGradientAreaY(325, 675, 10, 30, TerrainType.SEA, TerrainType.GRASS);
        fillGradientAreaY(675, XLIMIT - 10, 10, 30, TerrainType.SEA, TerrainType.ICE);
        fillGradientAreaY(10, 325, YLIMIT - 10, YLIMIT - 31, TerrainType.SEA2, TerrainType.DESERT);
        fillGradientAreaY(325, 675, YLIMIT - 10, YLIMIT - 31, TerrainType.SEA, TerrainType.GRASS);
        fillGradientAreaY(675, XLIMIT - 10, YLIMIT - 10, YLIMIT - 31, TerrainType.SEA, TerrainType.ICE);

        addLakes(20, 320, 20, YLIMIT - 20, 20, 30, TerrainType.SEA2);
        addLakes(320, 670, 20, YLIMIT - 20, 40, 30, TerrainType.SEA);
        addLakes(670, 980, 20, YLIMIT - 20, 40, 30, TerrainType.SEA);

        removeNarrows();

        addTrees(20, 320, 20, YLIMIT - 20, 100, 5, 0, TerrainType.DESERTWOOD1);
        addTrees(320, 670, 20, YLIMIT - 20, 100, 40, 5, TerrainType.GRASSWOOD1);
        addTrees(670, 980, 20, YLIMIT - 20, 100, 30, 5, TerrainType.ICEWOOD1);
        addTrees(20, 310, 20, YLIMIT - 20, 400, 5, 1, TerrainType.DESERTTREE1);
        addTrees(350, 640, 20, YLIMIT - 20, 400, 5, 1, TerrainType.GRASSTREE1);
        addTrees(680, 980, 20, YLIMIT - 20, 400, 5, 1, TerrainType.ICETREE1);

        addHills(20, 310, 20, YLIMIT - 20, 1000, TerrainType.DESERTHILL);
        addHills(350, 640, 20, YLIMIT - 20, 1000, TerrainType.GRASSHILL);
        addHills(680, 980, 20, YLIMIT - 20, 1000, TerrainType.ICEHILL);

        addHouses(20, 310, 20, YLIMIT - 20, 100, 3, TerrainType.DESERTHOUSE1);
        addHouses(350, 640, 20, YLIMIT - 20, 100, 3, TerrainType.GRASSHOUSE1);
        addHouses(680, 980, 20, YLIMIT - 20, 100, 1, TerrainType.ICEHOUSE1);

        addMountains(20, 310, 20, YLIMIT - 20, 400, 8, TerrainType.DESERTMOUNTAIN1);
        addMountains(350, 640, 20, YLIMIT - 20, 400, 8, TerrainType.GRASSMOUNTAIN1);
        addMountains(680, 980, 20, YLIMIT - 20, 400, 8, TerrainType.ICEMOUNTAIN1);


        PerfLogger pl = new PerfLogger(this, "Routing Roads");
        routeRoads(12);
        pl.report();

        doEdges();
    }


    private void fillArea(int xStart, int xEnd, int yStart, int yEnd, TerrainType terrain) {
        for (int x = xStart; x < xEnd; x++) {
            for (int y = yStart; y < yEnd; y++) {
                tileMap[x][y].setTerrain(terrain, BASE);
            }
        }
    }

    private void fillGradientAreaX(int xStart, int xEnd, int yStart, int yEnd, TerrainType terrain1, TerrainType terrain2) {

        int x = xStart;
        do {
            int y = yStart;
            do {
                if (tileMap[xStart < xEnd ? x - 1 : x + 1][y].isTerrainType(terrain1, BASE)) {
                    if (Utilities.roll(Math.abs(x - xStart), Math.abs(xEnd - xStart))) {
                        tileMap[x][y].setTerrain(terrain1, BASE);
                    } else {
                        tileMap[x][y].setTerrain(terrain2, BASE);
                    }
                } else {
                    tileMap[x][y].setTerrain(terrain2, BASE);
                }
            } while ((yStart < yEnd ? y++ : y--) != yEnd);
        } while ((xStart < xEnd ? x++ : x--) != xEnd);
    }

    private void fillGradientAreaY(int xStart, int xEnd, int yStart, int yEnd, TerrainType terrain1, TerrainType terrain2) {

        int y = yStart;
        do {
            int x = xStart;
            do {
                if (tileMap[x][yStart < yEnd ? y - 1 : y + 1].isTerrainType(terrain1, BASE)) {
                    if (Utilities.roll(Math.abs(y - yStart), Math.abs(yEnd - yStart))) {
                        tileMap[x][y].setTerrain(terrain1, BASE);
                    } else {
                        tileMap[x][y].setTerrain(terrain2, BASE);
                    }
                } else {
                    tileMap[x][y].setTerrain(terrain2, BASE);
                }
            } while ((xStart < xEnd ? x++ : x--) != xEnd);
        } while ((yStart < yEnd ? y++ : y--) != yEnd);
    }


    public void showMaskValues(int x, int y) {

        final ArrayList<Short> masks = new ArrayList<>();
        final Mask eMask = euclidianMask(x, y, tileMap[x][y].getTerrain(BASE), BASE);

        if (!masks.contains(eMask.mask)) {
            System.out.println("case " + eMask.mask + ":");
            masks.add(eMask.mask);
        }
    }


    private void removeNarrows() {

        for (int x = 1; x < XLIMIT - 1; x++) {
            for (int y = 1; y < YLIMIT - 1; y++) {

                TerrainType terrain = tileMap[x][y].getTerrain(BASE);
                Mask eMask = euclidianMask(x, y, terrain, BASE);

                if (eMask.adjTerrain != null) {
                    tileMap[x][y].setTerrain(terrain.removeNarrows(eMask.adjTerrain, eMask.mask), BASE);
                }
            }
        }
    }

    private void doEdges() {

        final ArrayList<Coords> bridges = new ArrayList<>();

        for (int x = 1; x < XLIMIT - 1; x++) {
            for (int y = 1; y < YLIMIT - 1; y++) {

                TerrainType terrain = tileMap[x][y].getTerrain(BASE);
                Mask eMask = euclidianMask(x, y, terrain, BASE);

                if (terrain != null) {

                    if (eMask.adjTerrain != null) {
                        tileMap[x][y].setTerrain(terrain.getEdging(eMask.adjTerrain, eMask.mask), BASE);
                    }

                    if (tileMap[x][y].isRiver()) {
                        tileMap[x][y].setTerrainType(TerrainType.getRiver(manhattanMask(x, y, true)), true, RIVER);
                    }

                    if (tileMap[x][y].isRoad()) {
                        tileMap[x][y].setTerrainType(TerrainType.getRoad(manhattanMask(x, y, false)), true, ROAD);
                    }

                    if (tileMap[x][y].isRiver() && tileMap[x][y].isRoad()) {
                        bridges.add(new Coords(x, y));
                    }
                }
            }
        }
        addBridges(bridges);
    }

    private void addBridges(ArrayList<Coords> bridges) {

        for (Coords coords : bridges) {

            int x = coords.getX();
            int y = coords.getY();

            if (tileMap[x][y].isBridgePassable() &&
                    tileMap[x - 1][y].isBridgePassable() &&
                    tileMap[x + 1][y].isBridgePassable()) {
                drawBridge(x, y, true);
            }

            if (tileMap[x][y].isBridgePassable() &&
                    tileMap[x][y - 1].isBridgePassable() &&
                    tileMap[x][y + 1].isBridgePassable()) {
                drawBridge(x, y, false);
            }

        }
    }

    private void drawBridge(int x, int y, boolean nswe) {
        TerrainType bridge = TerrainType.getBridge(tileMap[x][y].getTerrain(BASE), nswe);
        int i = 0;
        if (nswe) {
            for (BridgeNSSet bs : BridgeNSSet.values()) {
                tileMap[x + bs.getX()][y + bs.getY()].setTerrain(bridge.getNext(i++), TOP);
                tileMap[x + bs.getX()][y + bs.getY()].setBridge();
            }
        } else {
            for (BridgeWESet bs : BridgeWESet.values()) {
                tileMap[x + bs.getX()][y + bs.getY()].setTerrain(bridge.getNext(i++), TOP);
                tileMap[x + bs.getX()][y + bs.getY()].setBridge();
            }
        }
    }

    private void addLakes(int xStart, int xEnd, int yStart, int yEnd, int number, int maxSize, TerrainType terrain) {

        for (int i = 1; i < number; i++) {
            int size = Utilities.random(maxSize);

            int xLoc = Utilities.random(xStart + 10, xEnd - size - 10);
            int yLoc = Utilities.random(yStart + 10, yEnd - size - 10);

            int width = size / 2;

            for (int x = xLoc; x < xLoc + size; x++) {
                width = (x < xLoc + (size / 2)) ? width + 1 : width - 1;
                for (int y = Utilities.adjust(yLoc, 2); y < yLoc + Utilities.adjust(width, 2); y++) {
                    tileMap[x][y - (width / 2)].setTerrain(terrain, BASE);
                    Player.setLocX(x);
                    Player.setLocY(y);
                }
            }
        }
    }

    private void addTrees(int xStart, int xEnd, int yStart, int yEnd, int number, int maxSize, int randMax, TerrainType terrain) {

        for (int i = 1; i < number; i++) {
            int size = Utilities.random(maxSize);

            int xLoc = Utilities.random(xStart + 10, xEnd - size - 10);
            int yLoc = Utilities.random(yStart + 10, yEnd - size - 10);

            int width = size / 2;

            for (int x = xLoc; x < xLoc + size; x++) {
                width = (x < xLoc + (size / 2)) ? width + 1 : width - 1;
                for (int y = Utilities.adjust(yLoc, 2); y < yLoc + Utilities.adjust(width, 2); y++) {
                    tileMap[x][y - (width / 2)].setTerrainType(terrain.getNext(Utilities.random(0, randMax)), false, TOP);
                    Player.setLocX(x);
                    Player.setLocY(y);
                }
            }
        }
    }

    private void addHills(int xStart, int xEnd, int yStart, int yEnd, int number, TerrainType terrain) {
        for (int i = 1; i < number; i++) {
            tileMap[Utilities.random(xStart, xEnd)][Utilities.random(yStart, yEnd)].setTerrainType(terrain, false, TOP);
        }
    }

    private void addHouses(int xStart, int xEnd, int yStart, int yEnd, int number, int randMax, TerrainType terrain) {
        for (int i = 1; i < number; i++) {
            int x = Utilities.random(xStart, xEnd);
            int y = Utilities.random(yStart, yEnd);
            if (tileMap[x][y].setTerrainType(terrain.getNext(Utilities.random(0, randMax)), false, TOP)) {
                houses.add(new Coords(x, y));
            }
        }
    }

    private void routeRoads(int number) {

        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        if (!houses.isEmpty()) {
            for (int i = 0; i < number; i++) {
                executor.submit(new Runnable() {
                    @Override
                    public void run() {
                        Coords start = houses.remove(Utilities.random0(houses.size()));
                        Coords finish = houses.get(Utilities.random0(houses.size()));
                        buildRoad(start.getX(), start.getY(), finish.getX(), finish.getY());
                    }
                });
            }
        }

        try {
            executor.shutdown();
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            logger.log(Level.WARNING, "Road routing interrupted befor completion", e);
        }
    }

    private void buildRoad(int startx, int starty, int destx, int desty) {

        //System.out.println("Building a road from " + startx + "," + starty + " to " + destx + "," + desty);

        Router[][] nodes = new Router[XLIMIT][YLIMIT];

        for (int x = 0; x < XLIMIT; x++) {
            for (int y = 0; y < YLIMIT; y++) {
                nodes[x][y] = new Router(x, y);
            }
        }

        final PriorityQueue<Router> openList = new PriorityQueue<>();
        openList.clear();
        Router startNode = nodes[startx][starty];
        startNode.setDistanceFromStart(0);
        openList.add(startNode);

        while (!openList.isEmpty()) {
            Router current = openList.poll();
            if (current.getX() == destx && current.getY() == desty) {
                while (current != null) {
                    tileMap[current.getX()][current.getY()].setRoad();
                    current = current.getPreviousTile();
                }
                return;
            }
            current.setChecked();

            for (ManhattanSet ms : ManhattanSet.values()) {
                Router neighbour = nodes[current.getX() + ms.getX()][current.getY() + ms.getY()];
                if (neighbour.isChecked()) continue;
                Tile neighbourTile = tileMap[current.getX() + ms.getX()][current.getY() + ms.getY()];
                if (neighbourTile.isRoadPassable()) {
                    int neighborDistanceFromStart = current.getDistanceFromStart() + (tileMap[current.getX()][current.getY()].isRiver() && neighbourTile.isRiver() ? 8 : 1);
                    if (!openList.contains(neighbour) || neighborDistanceFromStart < current.getDistanceFromStart()) {
                        neighbour.setPreviousTile(current);
                        neighbour.setDistanceFromStart(neighborDistanceFromStart);
                        neighbour.setDistanceToTarget(Math.abs(neighbour.getX() - destx) + Math.abs(neighbour.getY() - desty));
                        openList.offer(neighbour);
                    }
                }
            }
        }

        logger.log(Level.WARNING, "Failed to route road!!!");
    }


    private void addMountains(int xStart, int xEnd, int yStart, int yEnd, int number, int maxSize, TerrainType terrain) {

        for (int i = 1; i < number; i++) {
            int size = Utilities.random(maxSize);
            int xLoc = Utilities.random(xStart + 10, xEnd - size - 10);
            int yLoc = Utilities.random(yStart + 10, yEnd - size - 10);
            int width = size / 2;

            ArrayList<Coords> riverStarts = new ArrayList<>();

            for (int x = xLoc; x < xLoc + size; x += 2) {
                width = (x < xLoc + (size / 2)) ? width + 1 : width - 1;
                mountain:
                for (int y = Utilities.adjust(yLoc, 2); y < yLoc + Utilities.adjust(width, 2); y += 2) {
                    for (MountainSet ms : MountainSet.values()) {
                        if (!tileMap[x + ms.getX()][y + ms.getY()].isMountainPassable()) {
                            continue mountain;
                        }
                    }

                    if (x == xLoc) {
                        riverStarts.add(new Coords(x + 2, y));
                    }

                    int j = 0;
                    for (MountainSet ms : MountainSet.values()) {
                        tileMap[x + ms.getX()][y + ms.getY()].setTerrainType(terrain.getNext(j++), false, TOP);
                        tileMap[x + ms.getX()][y + ms.getY()].setMountain();
                    }
                }
            }

            if (!riverStarts.isEmpty() && Utilities.roll(6, 8)) {
                boolean leftRight = Utilities.roll(4, 6);
                Coords start = riverStarts.get(Utilities.random0(riverStarts.size()));
                routeRiver(leftRight, start.getX(), start.getY());
            }
        }
    }

    private void routeRiver(boolean leftRight, int x, int y) {


        if (leftRight) {
            while (true) {
                int xTarget = x + Utilities.random(3) + 2;
                do {
                    if (!tileMap[x][y].isRiverPassable()) {
                        return;
                    }
                    tileMap[x][y].setRiver();
                } while (++x < xTarget);

                int yTarget = Utilities.adjust(y, 6);
                while (y != yTarget) {
                    if (!tileMap[x][y].isRiverPassable()) {
                        return;
                    }
                    tileMap[x][y].setRiver();
                    if (y < yTarget) {
                        y++;
                    } else {
                        y--;
                    }
                }
            }

        } else {
            while (true) {
                int yTarget = y - Utilities.random(3) - 2;
                do {
                    if (!tileMap[x][y].isRiverPassable()) {
                        return;
                    }
                    tileMap[x][y].setRiver();
                } while (--y > yTarget);

                int xTarget = Utilities.adjust(x, 6);
                while (x != xTarget) {
                    if (!tileMap[x][y].isRiverPassable()) {
                        return;
                    }
                    tileMap[x][y].setRiver();
                    if (x < xTarget) {
                        x++;
                    } else {
                        x--;
                    }
                }
            }
        }
    }


    private short manhattanMask(int xloc, int yloc, boolean river) {
        short mask = 0;

        for (int x = xloc - 1; x <= xloc + 1; x++) {
            for (int y = yloc - 1; y <= yloc + 1; y++) {
                if (x == xloc ^ y == yloc) {
                    if (river) {
                        if (tileMap[x][y].isRiver()) {
                            mask++;
                        }
                    } else {
                        if (tileMap[x][y].isRoad()) {
                            mask++;
                        }
                    }
                    mask <<= 1;
                }
            }
        }

        mask >>= 1;
        return mask;
    }

    private Mask euclidianMask(int x, int y, TerrainType terrain, int level) {
        short mask = 0;

        TerrainType adjTerrain = null;

        for (int i = x - 1; i <= x + 1; i++) {
            for (int j = y - 1; j <= y + 1; j++) {
                if (!(i == x && j == y)) {

                    if (tileMap[i][j].isTerrainType(terrain, level)) {
                        mask++;
                    } else {
                        adjTerrain = tileMap[i][j].getTerrain(level);
                    }
                    mask <<= 1;
                }
            }
        }

        mask >>= 1;
        return new Mask(mask, adjTerrain);
    }

}




